/*
 *   Copyright (C) 2014  Raghavan Santhanam, raghavanil4m@gmail.com.
 *   I wrote this DJit tool for fun at parties! Send any
 *   opinions/suggestions/bug reports to raghavanil4m@gmail.com.
 *
 *   djit.c is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   djit.c is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


/*
 * DJit - A very simple tool that randomizes the playing of songs in terms
 * of the song being chosen, the starting time, the ending time, with manual
 * enter-skip and true DJ effect of overlapping of the back to back songs
 * for few seconds!
 *
 * The default playback happens using vlc as backend. Reverse playback happens
 * using sox(http://sox.sourceforge.net/).
 *
 * The code looks little bulky for such a simple job since there are many
 * cases to handle overall! And everything is descriptive as "C" is! :)
 *
 * I wanted to do something crazy and engaging using my C skills. I happened
 * to write this code in a matter of two days. Actually, one late night
 * (Sep 19th, 2014) and one day(Sep 20th, 2014)! :)
 *
 * Platform used and tested upon is Ubuntu.
 *
 * ** Features **
 * 	-> Randomized song selection, song start and end offset.
 *	-> True DJ effect with overlapped play of back to back songs.
 *	-> Good number of songs selected. Typical recursive directory reading.
 *	-> Smoothest transition using <enter> to skip.
 *	-> Pause/continue supported.
 *	-> Favorite song history remembered: played fully -> play it fully.
 *	-> Randomized speed.
 *	-> Reverse play.
 *	-> Quick, simple and small.
 *	-> Uses vlc as backend for default playback.
 *	-> Uses sox as backend for reverse playback.
 *	-> Terminal user-friendly.
 *	-> Good error-handling.
 *
 * ** Usage **
 *	-> Build the source djit.c using C compiler of your choice.
 *	-> Launch the executable built with the path to the folder having songs.
 *	-> To manually move to next songs, press enter.
 *	-> Enter c to play current song fully.
 *	-> Enter p to pause current song.
 *	-> Enter to resume a paused song.
 *	-> To end playing, ctrl-c.
 */


#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <stdbool.h>
#include <poll.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <dirent.h>
#include <sys/stat.h>
#include <termios.h>

// #define __VERBOSE__

#ifdef __VERBOSE__
	#define printv(...) printf(__VA_ARGS__)
#else
	#define printv(...)
#endif
#define printd(...) printf(__VA_ARGS__)
#define err(f) fprintf(stderr, color "error: " #f " - %s" nocolor "\n", strerror(errno))
#define nocolor "\e[m"
#define color "\e[32m"
#define SONG_MAX (1024 * 1024)
struct songs {
	char dir[PATH_MAX];
	char song[NAME_MAX];
	bool played;
	bool fullyplayed;
} songs[SONG_MAX];
static int total;
static const char *player = "/usr/bin/vlc";
static const char *revplayer = "/usr/bin/play"; // sox
static const char *revplayerpackage = "sox";
#define telltoinstallplayer printd(color "\n\nCheck %s(for play). sudo apt-get install vlc." nocolor "\n\n", player)
#define telltoinstallrevplayer printd(color "\n\nCheck %s(for reverse). sudo apt-get install %s." nocolor "\n\n", revplayer, revplayerpackage)
#define MAX_TIME 45
#define MIN_TIME 35
#define OVERLAP_TIME 5
static const float speed[] = { 1.0f, 1.1f, 1.2f, 1.3f, 1.4f, 1.5f, 1.6f, 1.7f };
#define NUM_SPEEDS (sizeof(speed) / sizeof(speed[0]))
#define SPEED_CHANGE_INTERVAL 4
#define REVERSE_INTERVAL 6
static int nextsong = -1;
static const char *extensions[] = { "mp3", "wav", "wma", NULL };
static struct termios old;

static void speakout(void)
{
	(void)tcsetattr(STDIN_FILENO, TCSANOW, &old);
}

static void makesilent(void)
{
	int got = tcgetattr(STDIN_FILENO, &old);
	if (got < 0) {
		err(tcgetattr);
		exit(EXIT_FAILURE);
	}
	struct termios new = old;
	new.c_lflag &= ~ECHO;
	int gave = tcsetattr(STDIN_FILENO, TCSANOW, &new);
	if (gave < 0) {
		err(tcsetattr);
		exit(EXIT_FAILURE);
	}

	int speakable = atexit(speakout);
	if (speakable < 0) {
		err(atexit);
		exit(EXIT_FAILURE);
	}
}

static bool issong(const char *song)
{
	printv("song : %s\n", song);

	bool y = false;
	int i = 0;
	while (extensions[i]) {
		if (strstr(song, extensions[i])) {
			y = true;
			break;
		}
		i++;
	}

	return y;
}

static void update(const char *dir)
{
	DIR *d = opendir(dir);
	if (!d) {
		err(opendir);
		exit(EXIT_FAILURE);
	}
	static int i = 0;
	while (i < SONG_MAX) {
		struct dirent *f = readdir(d);
		if (!f) {
			break;
		}
		if (!strcmp(f->d_name, ".") ||
			!strcmp(f->d_name, "..")) {
			continue;
		}
		char p[PATH_MAX] = "";
		strcpy(p, dir);
		strcat(p, "/");
		strcat(p, f->d_name);
		struct stat st = { 0 };
		int r = stat(p, &st);
		if (r < 0) {
			err(stat);
			exit(EXIT_FAILURE);
		}
		if (S_ISDIR(st.st_mode)) {
			update(p);
		} else if (S_ISREG(st.st_mode) &&
					issong(f->d_name)) {
			strcpy(songs[i].dir, dir);
			strcpy(songs[i].song, f->d_name);
			i++;
		}
	}
	total = i;
	if (!total) {
		printd("No songs!\n");
		exit(EXIT_SUCCESS);
	}

	(void)closedir(d);
}

static void print(void)
{
	printd("\n\t** Playlist **\n\n");
	int i = 0;
	while (i < total) {
		if (!songs[i].song) {
			break;
		}
		printd("%d" color " -> " nocolor "%s\n", i, songs[i].song);
		i++;
	}
	printd("\n\t ** Playlist **\n\n");
}

static void clearall(void)
{
	int i = 0;
	while (i < total) {
		songs[i++].played = false;
	}
}

static bool activesleep(int time, pid_t p, bool *skipped, bool *continuee, bool *pausee)
{
	bool loopforever = time == -1;
	struct pollfd pfd[] = { { STDIN_FILENO, POLLIN } };
	int sleeptime = time * 1000;
	int slepttime = 10;
	while (1) {
		int pollit = poll(pfd, 1, slepttime);
		if (pollit < 0) {
			err(pollit);
			exit(EXIT_FAILURE);
		}
		if (pfd[0].revents == POLLIN) {
			char b[BUFSIZ] = ""; // Empty significant stdin bytes.
			ssize_t r = read(STDIN_FILENO, b, sizeof(b));
			if (continuee && r > 0 && (b[0] == 'c' || b[0] == 'C')) {
				// Let the current song continue.
				*continuee = true;
			} else if (pausee && r > 0 && (b[0] == 'p' || b[0] == 'P')) {
				// Let the current song pause.
				*pausee = true;
			} else if (skipped) {
				// Skip immediately.
				printv("Skipped.\n");
				*skipped = true;
			}
			break;
		} else if (p != (pid_t)-1) {
			// Check if the song is being still played.
			int exited = waitpid(p, NULL, WNOHANG);
			if (exited < 0) {
				printv("Short song!\n");
				return false;
			}
		}
		if (!loopforever) { // Overlap or default playtime.
			sleeptime -= slepttime;
			if (sleeptime < 0) {
				break;
			}
		}
	}

	return true;
}

static void pauseandresume(pid_t p)
{
	printv("Paused.\n");
	kill(p, SIGSTOP);
	if (activesleep(-1, (pid_t)-1, NULL, NULL, NULL)) {
		kill(p, SIGCONT);
	}
	printv("Resumed.\n");
}

static void play(int song, char *envp[])
{
	printd(color "Now : " nocolor "%s" color " Next : " nocolor "%s",
					songs[song].song, songs[nextsong].song);
	fflush(stdout);

	char songpath[PATH_MAX] = "";
	strcpy(songpath, songs[song].dir);
	strcat(songpath, "/");
	strcat(songpath, songs[song].song);
	printv("songpath : %s\n", songpath);

	int playtime = rand() % MAX_TIME;
	if (playtime < MIN_TIME) {
		playtime = MIN_TIME;
	}
	static int speedchangecount = 0;
	float s = 1.0f;
	if (speedchangecount == SPEED_CHANGE_INTERVAL - 1) {
		s = speed[rand() % NUM_SPEEDS];
	}
	speedchangecount = (speedchangecount + 1) % SPEED_CHANGE_INTERVAL;

	static int revcount = 0;
	bool revit = false;
	if (revcount == REVERSE_INTERVAL - 1) {
		revit = true;
	}
	revcount = (revcount + 1) % REVERSE_INTERVAL;

	pid_t p = fork();
	if (p == (pid_t)-1) {
		err(fork);
		exit(EXIT_FAILURE);
	}

	if (p) {
		printv("VLC pid : %d\n", p);
		printv("Playing for %d seconds\n", playtime);
		static bool lastsongskipped = false;
		static pid_t lastsong = (pid_t)-1;
		// Overlap logic.
		if (lastsong != (pid_t)-1) {
			// Continue logic doesn't apply to overlap.
			bool skipcurrent = false;
			if (!lastsongskipped) { // Overlap only when not skipped.
				int overlaptime = OVERLAP_TIME;
				if (revit) {
					overlaptime = OVERLAP_TIME * 2 + 2;
				}
				if (activesleep(overlaptime, (pid_t)-1, &skipcurrent, NULL, NULL)) {
					// Still playing.
					if (skipcurrent) {
						kill(p, SIGKILL);
						p = (pid_t)-1;
					}
				}
			}

			kill(lastsong, SIGKILL);
			lastsong = (pid_t)-1;

			if (skipcurrent) {
				printd("\n");
				return;
			}
		}

		lastsongskipped = false;
		lastsong = p;

		bool continuedtillend = false;
		// Handles play, pause and continue combinations.
		while (1) {
			bool continuee = false;
			bool pausee = false;
			if (!songs[song].fullyplayed && // Check history.
				!activesleep(playtime, p, &lastsongskipped, &continuee, &pausee)) {
				// Play using default playtime.
				lastsong = (pid_t)-1;
				break;
			} else if (songs[song].fullyplayed || continuee) { // Current song continues.
				printv("Playing fully.\n");
				if (!activesleep(-1, p, &lastsongskipped, NULL, &pausee)) {
					// Current song ended after playing to end.
					continuedtillend = true;
					lastsong = (pid_t)-1;
				} else if (pausee) {
					// When resumed continue normally.
					pauseandresume(p);
					continue;
				} else {
					// Current song skipped.
					kill(p, SIGKILL);
					lastsong = (pid_t)-1;
				}
				break;
			} else if (pausee) {
				pauseandresume(p);
 			} else {
				// Next song.
				break;
			}
		}

		printd("\n");
		if (!lastsongskipped) {
			printv("Played fully.\n");
		}
		if (continuedtillend) {
			printv("Continued till end.\n");
			songs[song].fullyplayed = true;
		} else {
			printv("Continued but not till end.\n");
			songs[song].fullyplayed = false;
		}
	} else {
		printv("Playing song\n");
		if (revit) {
			char *args[] = {	(char *)revplayer,
						songpath,
						(char *)"reverse",
						NULL,
					};
			fclose(stderr);
			fclose(stdin);
			execve(revplayer, args, envp);
			printv(color "\n\nFailed to reverse. sox." nocolor "\n\n");
		}

		srand(time(NULL));
		int starttime = rand() % playtime;
		printv("Starting at %d seconds\n", starttime);
		char starttimeoption[100] = "";
		sprintf(starttimeoption, "--start-time=%d", starttime);
		char rate[100] = "";
		sprintf(rate, "--rate=%.1f", s);
		char *args[] = { 	(char *)player,
					(char *)"-Idummy",
					(char *)"--play-and-exit",
					starttimeoption,
					songpath,
					rate,
					NULL
				};
		fclose(stderr);
		fclose(stdin);
		(void)execve(player, args, envp);
		printd(color "\n\nFailed to play. vlc\n\n" nocolor);
		err(execve);
		exit(EXIT_FAILURE);
	}
}

static int songnumber(int *songnum)
{
	if (total == 1) {
		return 0;
	}

	int newsong = -1;
	int oldsong;

	if (songnum && *songnum != -1) {
		oldsong = *songnum;
		newsong = rand() % total;
		while (newsong == oldsong || songs[newsong].played) {
			usleep(100);
			newsong = rand() % total;
		}
	} else {
		newsong = rand() % total;
		while (songs[newsong].played) {
			usleep(100);
			newsong = rand() % total;
		}
	}

	return newsong;
}

static void djit(char *envp[])
{
	int played = 0;
	srand(time(NULL));
	while (1) {
		int cursong = nextsong == -1 ? songnumber(NULL) : nextsong;
		nextsong = songnumber(&cursong);

		printv("%d -> %s\n", cursong, songs[cursong].song);
		songs[cursong].played = true;
		played++;
		play(cursong, envp);
		if (played == total - 1) {
			printv("Cycle complete.\n");
			clearall();
			played = 0;
		}
		usleep(100);
	}	
}

static void stop(int sig)
{
	printd("\n");
	exit(EXIT_SUCCESS);
}

static void tips(void)
{
	printf(	"\n Useful tips : \n"
		"\t=> To manually move to next songs, press enter.\n"
		"\t=> Enter c to play current song fully.\n"
		"\t=> Enter p to pause current song.\n"
		"\t=> Enter to resume a paused song.\n"
		"\t=> To end playing, ctrl-c.\n"
		"\n");
}

static void boostego(void)
{
	printd("\n** " color "DJit" nocolor " by " color "Raghavan Santhanam"
		nocolor " - raghavanil number4 m at gmail period com **\n\n");
}

static void preparetostop(void)
{
	signal(SIGINT, stop);
}

static void checktools(void)
{
	if (access(player, R_OK | X_OK)) {
		telltoinstallplayer;
		err(access);
		exit(EXIT_FAILURE);
	}
	if (access(revplayer, R_OK | X_OK)) {
		telltoinstallrevplayer;
		err(access);
		exit(EXIT_FAILURE);
	}
}

int main(int argc, char *argv[], char *envp[])
{
	boostego();
	if (argc < 2) {
		printd("Usage: DJit dir\n");
		return -1;
	}
	tips();
	checktools();
	preparetostop();
	update(argv[1]);
	print();
	boostego();
	tips();
	makesilent();
	djit(envp);

	return 0;
}

